using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace ModelViewer
{
    /// <summary>
    /// Implements a camera model for XNA applications.  The camera can be 
    /// configured as a target camera or placed using 3D Studio Max rotation
    /// matrix values at a specific location.
    /// </summary>
    public class Camera
    {    
        #region Fields
        
        /// <summary>
        /// The projection matrix computed for the current view
        /// </summary>
        private Matrix projection;

        /// <summary>
        /// The view matrix for the current camera view
        /// </summary>
        private Matrix view;

        private Vector3 eye = new Vector3(0, 0, 10);
        private Vector3 center = new Vector3(0, 0, 0);
        private Vector3 up = new Vector3(0, 1, 0);
        private float aspect = 1;
        private float fov = MathHelper.ToRadians(30);
        private bool gravity = true;

        private MouseMode mouseMode1 = MouseMode.PITCHYAW;
        private MouseMode mouseMode2 = MouseMode.PANTILT;
        private Vector3 rotationXZY = Vector3.Zero;         // 3D Studio Max style rotation
        private Vector3 rotationXYZ = Vector3.Zero;         // Maya style rotation
        private Quaternion rotation = Quaternion.Identity;  // Quaternion specification

        private CameraModes specMode = CameraModes.LookAt;        // Specification type
        private float zNear = 10;
        private float zFar = 3000;
        private BoundingFrustum boundingFrustum = null;     // Bounding frustum corresponding
                                                            // to the current camera setting

        /// <summary>
        /// A game this camera is associated with
        /// </summary>
        private Game game = null;

        /// <summary>
        /// Automatic depth computations
        /// </summary>
        private bool autoDepth = false;

        #endregion

        #region Properties
        /// <summary>
        /// Camera orientation configuration specification type
        /// </summary>
        public enum CameraModes { 
            /// <summary>
            /// Camera orientation is specified using LookAt type parameters
            /// using the camera center, eye, and up direction
            /// </summary>
            LookAt, 
            
            /// <summary>
            /// Camera orientation is specified using a vector wtih
            /// rotations supplied in XZY order.  This is the standard
            /// ordering for 3DS Max rotation imports.
            /// </summary>
            RotationXZY,
        
            /// <summary>
            /// Camera orientation is specified using a vector with 
            /// rotations supplied in XYZ order.  
            /// </summary>
            RotationXYZ,

            /// <summary>
            /// Camera rotation is specified by a quaterion
            /// </summary>
            RotationQuaternion
        }

        /// <summary>
        /// The mouse mode assigned to a button if we use the
        /// mouse to manipulate the view.
        /// </summary>
        public enum MouseMode { 
            /// <summary>
            /// Ignore this button
            /// </summary>
            NONE,

            /// <summary>
            /// Pan/Tilt
            /// </summary>
            PANTILT, 
            
            /// <summary>
            /// Pitch/Yaw
            /// </summary>
            PITCHYAW,

			/// <summary>
			/// Pitch/Yaw with the pitch inverted
			/// </summary>
			PITCHYAWINV
        }

        /// <summary>
        /// Computed camera projection matrix
        /// </summary>
        public Matrix Projection { get { return projection; } set { projection = value; } }

        /// <summary>
        /// Computed camera view matrix
        /// </summary>
        public Matrix View { get { return view; } }

        /// <summary>
        /// Camera eye location
        /// </summary>
        public Vector3 Eye { get { return eye; } set { eye = value; Set(); } }

        /// <summary>
        /// Camera center
        /// </summary>
        public Vector3 Center { get { return center; } set { center = value; specMode = CameraModes.LookAt; Set(); } }

        /// <summary>
        /// Camera up vector
        /// </summary>
        public Vector3 Up { get { return up; } set { up = value; specMode = CameraModes.LookAt; Set(); } }

        /// <summary>
        /// Camera field of view (radians)
        /// </summary>
        public float Fov { get { return fov; } set { fov = value; Set();  } }

        /// <summary>
        /// Camera gravity flag (forces up a the Y direction)
        /// </summary>
        public bool Gravity { get { return gravity; } set { gravity = value; if (gravity) up = new Vector3(0, 1, 0); } }

        /// <summary>
        /// Camera mouse mode for button 1
        /// </summary>
        public MouseMode MouseMode1 { get { return mouseMode1; } set { mouseMode1 = value; } }

        /// <summary>
        /// Camera mouse mode for button 2
        /// </summary>
        public MouseMode MouseMode2 { get { return mouseMode2; } set { mouseMode2 = value; } }

        /// <summary>
        /// Computed distance from the camera to the center
        /// </summary>
        public float CameraDistance { get {Vector3 view = center - eye;  return view.Length();}}

        /// <summary>
        /// Camera aspect ratio
        /// </summary>
        public float Aspect { get { return aspect; } set { aspect = value; Set(); } }

        /// <summary>
        /// Near clipping plane distance
        /// </summary>
        public float ZNear { 
            get 
            {
                return autoDepth ? CameraDistance * 0.1f : zNear; 
            } 
            set 
            { 
                zNear = value;
                if (zFar <= zNear)
                    zFar = zNear * 2;
                Set(); 
            } }

        /// <summary>
        /// Far clipping plane distance
        /// </summary>
        public float ZFar 
        { 
            get 
            {
                return autoDepth ? CameraDistance * 10.0f : zFar;
            } 
            set 
            { 
                zFar = value;
                if (zNear >= zFar)
                    zNear = zFar / 2;
                Set(); 
            } }


        /// <summary>
        /// Specifies a camera rotation in the format 3D Studio Max uses.  This
        /// is a rotation in the order X, Y, Z, where 0, 0, 0 is facing down.
        /// Angles are in degrees.
        /// </summary>
        public Vector3 Rotation3DS
        {
            set
            {
                // 3D Studio Max uses the Z vector as the up direction when
                // specifying the Camera orientation.  This means we have
                // to swap Y and Z and the new Z value is negated.
                rotationXZY = new Vector3(MathHelper.ToRadians(value.X - 90),
                    MathHelper.ToRadians(value.Z),
                    -MathHelper.ToRadians(value.Y));
                specMode = CameraModes.RotationXZY;
                Set();
            }
        }

        /// <summary>
        /// Specifies a camera rotation in XYZ order.  This
        /// is a rotation in the order XYZ, where 0, 0, 0 is facing in
        /// the -Z direction.
        /// Angles are in degrees.
        /// </summary>
        public Vector3 RotationXYZ
        {
            set
            {
                // 3D Studio Max uses the Z vector as the up direction when
                // specifying the Camera orientation.  This means we have
                // to swap Y and Z and the new Z value is negated.
                rotationXZY = new Vector3(MathHelper.ToRadians(value.X),
                    MathHelper.ToRadians(value.Y),
                    MathHelper.ToRadians(value.Z));
                specMode = CameraModes.RotationXYZ;
                Set();
            }
        }

        /// <summary>
        /// Specifies a camera rotation as a quaternion.  Get will always
        /// works for any current view matrix.
        /// </summary>
        public Quaternion Rotation
        {
            set
            {
                rotation = value;
                specMode = CameraModes.RotationQuaternion;
                Set();
            }
            get
            {
                return Quaternion.Inverse(Quaternion.CreateFromRotationMatrix(View));
            }
        }

        /// <summary>
        /// A bounding frustum for the camera which is used to determine
        /// visiblity.
        /// </summary>
        public BoundingFrustum BoundingFrustum { get { return boundingFrustum; } }


        /// <summary>
        /// If set true, the camera znear and zfar are set automatically
        /// </summary>
        public bool AutoDepth { get { return autoDepth; } set { autoDepth = value; } }

        #endregion

        #region Construction and Initialization

        public Camera()
        {
        }

        public Camera(Game game)
        {
            this.game = game;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Make the projection and view matrices match the current camera parameters.
        /// </summary>
        private void Set()
        {
            // How far from camera to center?
            Vector3 v = center - eye;
            float len = v.Length();

            // Create the projection and view matrices
            projection = Matrix.CreatePerspectiveFieldOfView(fov, aspect, ZNear, ZFar);

            switch (specMode)
            {
                case CameraModes.LookAt:
                    view = Matrix.CreateLookAt(eye, center, up);
                    break;

                case CameraModes.RotationXYZ:
                    // In this case the rotation is supplied as Euler angles in the order 
                    // X, Y, and Z and that is assumed to indicate the orientation, so 
                    // our resulting matrix needs to be the inverse of that rotation
                    // (rotate the world relative to the camera).
                    view = Matrix.CreateTranslation(-eye) *
                        Matrix.CreateRotationZ(-rotationXZY.Z) *
                        Matrix.CreateRotationY(-rotationXZY.Y) *
                        Matrix.CreateRotationX(-rotationXZY.X);
                    break;

                case CameraModes.RotationXZY:
                    // In this case the rotation is supplied as Euler angles in the order 
                    // X, Z, and Y and that is assumed to indicate the orientation, so 
                    // our resulting matrix needs to be the inverse of that rotation
                    // (rotate the world relative to the camera).
                    view = Matrix.CreateTranslation(-eye) *
                        Matrix.CreateRotationY(-rotationXZY.Y) *
                        Matrix.CreateRotationZ(-rotationXZY.Z) *
                        Matrix.CreateRotationX(-rotationXZY.X);
                    break;

                case CameraModes.RotationQuaternion:
                    view = Matrix.CreateTranslation(-eye) * 
                        Matrix.CreateFromQuaternion(Quaternion.Inverse(rotation));
                    break;
            }

            // Compute the associated bounding frustum
            boundingFrustum = new BoundingFrustum(view * projection);
        }

        /// <summary>
        /// Set the field of view as a horizontal angle from left to right.  This converts
        /// the horizontal field of view to a vertical field of view, which is a bit
        /// more tolerant of varying screen widths.  This is the format 3D Studio Max uses
        /// for field of view.  Use the FieldOfView and FilmAspectRatio properties from
        /// the FBX file.
        /// </summary>
        /// <param name="fov"></param>
        /// <param name="aspect"></param>
        public void HorizontalFov(float fov, float aspect)
        {
            this.fov = 2 * (float)Math.Atan(Math.Tan(fov / 2) / aspect);
            Set();
        }


        /// <summary>
        /// Pitch is a rotation around center  and a vector in the camera X axis direction
        /// </summary>
        /// <param name="angle"></param>
        public void Pitch(float angle)
        {
            // Need a vector in the camera X direction
            Vector3 cameraZ = eye - center;  
            Vector3 cameraX = Vector3.Cross(up, cameraZ);
            float len = cameraX.LengthSquared();
            if (len > 0)
                cameraX.Normalize();
            else
                cameraX = new Vector3(1, 0, 0);

            Matrix t1 = Matrix.CreateTranslation(-center);
            Matrix r = Matrix.CreateFromAxisAngle(cameraX, angle);
            Matrix t2 = Matrix.CreateTranslation(center);

            Matrix M = t1 * r * t2;
            eye = Vector3.Transform(eye, M);
            if (!gravity)
                up= Vector3.Transform(up, M);
            Set();
        }

        /// <summary>
        /// Yaw is a rotation around the center and a vector in the camera Y axis direction.
        /// </summary>
        /// <param name="angle"></param>
        public void Yaw(float angle)
        {
            // Need a vector in the camera X direction
            Vector3 view = center - eye;    // Direction we are looking
            Vector3 cameraX = Vector3.Cross(up, view);
            float len = cameraX.LengthSquared();
            if (len > 0)
                cameraX.Normalize();
            else
                cameraX = new Vector3(1, 0, 0);

            Vector3 cameraY = Vector3.Cross(view, cameraX);
            cameraY.Normalize();

            Matrix t1 = Matrix.CreateTranslation(-center);
            Matrix t2 = Matrix.CreateTranslation(center);
            Matrix r = Matrix.CreateFromAxisAngle(cameraY, angle);

            Matrix M = t1 * r * t2;
            eye = Vector3.Transform(eye, M);
            if (!gravity)
                up = Vector3.Transform(up, M);
            Set();
        }


        /// <summary>
        /// MoveIn moves the camera to/from the center.
        /// </summary>
        /// <param name="z">Actual amout to move.  Positive is towards center.</param>
        public void MoveIn(float z)
        {
            // Vector in camera view direction
            Vector3 view = center - eye;    // Direction we are looking
            // What is the closest we want to allow the camera to move
            // This sets that value to 10% of the current distance.
            float dist = view.Length();

            float mindist = dist * 0.1f;     // How far away is it?
            if (z > mindist)
                z = mindist;

            // What is the maximum distance we will allow the camera to move away
            float maxdist = (float)1.0e8 - dist;
            if (z < -maxdist)
                z = -maxdist;

            eye += view * z/dist;                // Move the eye in the view direction
            Set();
        }


        /// <summary>
        /// Pan is rotation around the camera up direction and camera eye.
        /// </summary>
        /// <param name="angle"></param>
        public void Pan(float angle)
        {
            // Need a vector in the camera X direction
            Vector3 view = center - eye;    // Direction we are looking
            Vector3 cameraX = Vector3.Cross(up, view);
            float len = cameraX.LengthSquared();
            if (len > 0)
                cameraX.Normalize();
            else
                cameraX = new Vector3(1, 0, 0);

            Vector3 cameraY = Vector3.Cross(view, cameraX);
            cameraY.Normalize();

            Matrix t1 = Matrix.CreateTranslation(-eye);
            Matrix t2 = Matrix.CreateTranslation(eye);
            Matrix r = Matrix.CreateFromAxisAngle(cameraY, angle);

            Matrix M = t1 * r * t2;
            center = Vector3.Transform(center, M);
            Set();
        }


        /// <summary>
        /// Tilt is rotation around the camera X axis and the camera eye.
        /// </summary>
        /// <param name="angle"></param>
        public void Tilt(float angle)
        {
            // Need a vector in the camera X direction
            Vector3 cameraZ = eye - center;    // Direction we are looking
            Vector3 cameraX = Vector3.Cross(up, cameraZ);
            float len = cameraX.LengthSquared();
            if (len > 0)
                cameraX.Normalize();
            else
                cameraX = new Vector3(1, 0, 0);

            Matrix t1 = Matrix.CreateTranslation(-eye);
            Matrix r = Matrix.CreateFromAxisAngle(cameraX, angle);
            Matrix t2 = Matrix.CreateTranslation(eye);

            Matrix M = t1 * r * t2;
            center = Vector3.Transform(center, M);
            Set();
        }



        #endregion

        #region Mouse Controls

        private MouseState mouseLast = new MouseState(0, 0, 0, 
            ButtonState.Released, ButtonState.Released, ButtonState.Released, 
            ButtonState.Released, ButtonState.Released);

        /// <summary>
        /// Handle a mouse update based on a new mouse state.
        /// </summary>
        /// <param name="mouse"></param>
        public void Mouse(MouseState mouse)
        {
            if (game != null)
            {
                if (mouse.X < 0 || mouse.X >= game.GraphicsDevice.Viewport.Width ||
                    mouse.Y < 0 || mouse.Y >= game.GraphicsDevice.Viewport.Height)
                {
                    mouseLast = mouse;
                    return;
                }
            }

            // We are looking for a state where a button is pressed
            // now AND in the previous pass.

            // Left button
            if (mouse.LeftButton == ButtonState.Pressed && mouseLast.LeftButton == ButtonState.Pressed)
            {
                MouseAction(mouse, mouseMode1);
            }

            // Right button
            if (mouse.RightButton == ButtonState.Pressed && mouseLast.RightButton == ButtonState.Pressed)
            {
                MouseAction(mouse, mouseMode2);
            }

            // Mouse wheel
            if (mouse.ScrollWheelValue != mouseLast.ScrollWheelValue)
            {
                MouseWheelAction(mouse.ScrollWheelValue - mouseLast.ScrollWheelValue);
            }

            mouseLast = mouse;
        }

        public void MouseWheelAction(float delta)
        {
            float move = delta * CameraDistance;
            if (move > 0)
                MoveIn(move * 0.005f);
            else
                MoveIn(move * 0.005f);
        }

        /// <summary>
        /// Actually perform the desired mouse behavior.
        /// </summary>
        /// <param name="mouse"></param>
        /// <param name="mode"></param>
        private void MouseAction(MouseState mouse, MouseMode mode)
        {
            int dx = mouse.X - mouseLast.X;
            int dy = mouse.Y - mouseLast.Y;
            MouseAction(dx, dy, mode);
        }

        /// <summary>
        /// Perform mouse actions. This version can be called
        /// by clients that don't use the XNA mouse system.
        /// </summary>
        /// <param name="dx">Delta X</param>
        /// <param name="dy">Delta Y</param>
        /// <param name="mode">The mouse mode</param>
        public void MouseAction(int dx, int dy, MouseMode mode)
        {
            switch (mode)
            {
				case MouseMode.PITCHYAW:
					Yaw(dx * 0.003f);
					Pitch(-dy * 0.003f);
					break;

				case MouseMode.PITCHYAWINV:
					Yaw(dx * 0.003f);
					Pitch(dy * 0.003f);
					break;

				case MouseMode.PANTILT:
                    Pan(dx * 0.003f);
                    Tilt(dy * 0.003f);
                    break;
            }        
        }

        #endregion
    }
}
